package broadcast;

import java.security.AlgorithmParameters;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidParameterSpecException;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.zip.Deflater;
import java.util.zip.Inflater;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import common.Entity;
import common.Message;


import utils.ByteArrayManip;


class Serializer{
	private static final String SECRET = "qlwbEW9sRENi48RF";
	private static final int SOURCE_ID_OFFSET = 0;
	private static final int SOURCE_IP_OFFSET = 4;
	private static final int DEST_ID_OFFSET = 8;
	private static final int DEST_IP_OFFSET = 12;
	private static final int SEQ_OFFSET = 16;
	private static final int NUM_OF_PARTS_OFFSET = 20;
	private static final int PARTS_SEQ_OFFSET = 24;
	private static final int VIEW_NUM_OFFSET = 28;
	private static final int PAYLOAD_OFFSET = 32;
	
	public static final int MAXLENGTH = 1024;
	public static final int MAX_PAYLOAD_LENGTH = MAXLENGTH - PAYLOAD_OFFSET;
	
	//maybe add compression later
//	private static Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
//	private static Inflater inflater = new Inflater();
	
	private String secret;
	private Cipher coder;
	private Cipher decoder;
	private Key key;
	private Map<Entity, MsgDataPerEntity> messages;
	
	public Serializer(String k, String cipherTransformation) {
		secret = k;
		key = new SecretKeySpec(secret.getBytes(), "AES");
		messages = new HashMap<Entity, MsgDataPerEntity>();
		try {
//			c = Cipher.getInstance("AES/CFB/PKCS5Padding");
			coder = Cipher.getInstance(cipherTransformation);
			coder.init(Cipher.ENCRYPT_MODE, key);
			decoder = Cipher.getInstance(cipherTransformation);
			AlgorithmParameters params = coder.getParameters();
			byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV();
			decoder.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NoSuchPaddingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvalidAlgorithmParameterException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvalidParameterSpecException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	
	// if the message is longer than MAX_PAYLOAD_LENGTH, then it's split
	// the last array is not necessarily the same length as MAX_PAYLOAD_LENGTH
	public synchronized byte[][] split(Message m){
		byte[] data = m.getData();
		int msgLength = data.length;
		int numOfParts = (int)Math.ceil((double)msgLength / (double)MAX_PAYLOAD_LENGTH);
		byte[][] res = new byte[numOfParts][];
		int currentOffset = 0;
		for(int i = 0; i < numOfParts; i ++) {
			byte[] currentMsgBody;
			if(i == numOfParts - 1) {
				currentMsgBody = new byte[msgLength - i * MAX_PAYLOAD_LENGTH + PAYLOAD_OFFSET];
				System.arraycopy(m.getData(), currentOffset, currentMsgBody, PAYLOAD_OFFSET, msgLength - i * MAX_PAYLOAD_LENGTH);
			} else {
				currentMsgBody = new byte[MAXLENGTH];
				System.arraycopy(m.getData(), currentOffset, currentMsgBody, PAYLOAD_OFFSET, MAX_PAYLOAD_LENGTH);
				currentOffset += MAX_PAYLOAD_LENGTH;
			}
			//add source
			if(m.getDest()==null) {
				System.out.println("dest null");
			}
			if(m.getDest().getId() > 3 || m.getDest().getId() < 0) {
				System.out.println("illegal id set");
			}
			ByteArrayManip.addIntToByteArray(m.getSource().getId(), currentMsgBody, SOURCE_ID_OFFSET);
			ByteArrayManip.addIpToByteArray(m.getSource().getIp(), currentMsgBody, SOURCE_IP_OFFSET);
			//add dest
			ByteArrayManip.addIntToByteArray(m.getDest().getId(), currentMsgBody, DEST_ID_OFFSET);
			ByteArrayManip.addIpToByteArray(m.getDest().getIp(), currentMsgBody, DEST_IP_OFFSET);
			//add seq
			ByteArrayManip.addIntToByteArray(m.getSeq(), currentMsgBody, SEQ_OFFSET);
			//add numOfParts
			ByteArrayManip.addIntToByteArray(numOfParts, currentMsgBody, NUM_OF_PARTS_OFFSET);
			//add partsSeq
			ByteArrayManip.addIntToByteArray(m.getViewNum(), currentMsgBody, VIEW_NUM_OFFSET);
			ByteArrayManip.addIntToByteArray(i, currentMsgBody, PARTS_SEQ_OFFSET);
//			try {
//				res[i] = coder.doFinal(currentMsgBody);
//			} catch (IllegalBlockSizeException e) {
//				// TODO Auto-generated catch block
//				e.printStackTrace();
//			} catch (BadPaddingException e) {
//				// TODO Auto-generated catch block
//				e.printStackTrace();
//			}
			res[i] = currentMsgBody;
		}
		return res;
	}
	
	// merge gives back a complete Message object, with no fields null;
	// it gives back a null object when not all data has been received.
	public synchronized Message merge(byte[] data) {
		byte[] plain = data;
//		byte[] plain = null;
//		try {
//			plain = decoder.doFinal(data);
//		} catch (IllegalBlockSizeException e) {
//			// TODO Auto-generated catch block
//			e.printStackTrace();
//			return null;
//		} catch (BadPaddingException e) {
//			// TODO Auto-generated catch block
//			e.printStackTrace();
//			return null;
//		}
		if(plain != null) {
			int sourceId = ByteArrayManip.getIntFromByteArray(plain, SOURCE_ID_OFFSET);
			String sourceIp = ByteArrayManip.getIpFromByteArray(plain, SOURCE_IP_OFFSET);
			int seq = ByteArrayManip.getIntFromByteArray(plain, SEQ_OFFSET);
			int view = ByteArrayManip.getIntFromByteArray(plain, VIEW_NUM_OFFSET);
			int destId = ByteArrayManip.getIntFromByteArray(plain, DEST_ID_OFFSET);
			String destIp = ByteArrayManip.getIpFromByteArray(plain, DEST_IP_OFFSET);
			if(destId > 3 || destId < 0) {
				System.out.println("illegal destId");
			}
			Entity source = new Entity(sourceId, sourceIp);
			Entity dest = new Entity(destId, destIp);
			
			MsgDataPerEntity m = messages.get(source);
			if(m == null) {
				m = new MsgDataPerEntity();
				messages.put(source, m);
			} 
			m.put(plain);
			if(m.isFullMessage(seq)) {
				byte[][] value = m.getMsgData(seq);
				byte[] msgData = doMerge(value);
				Message res = new Message(msgData);
				res.setSource(source);
				res.setDest(dest);
				res.setSeq(seq);
				res.setViewNum(view);
				return res;
			}
		}
		return null;
	}
	
	
	// returns the orignal message's data array
	private byte[] doMerge(byte[][] value) {
		int totalLength = 0;
		for(int i = 0; i < value.length; i ++) {
			totalLength += value[i].length;
		}
		byte[] messageData = new byte[totalLength];
		for(int i = 0; i < value.length; i ++) {
			System.arraycopy(value[i], 0, messageData, i*MAX_PAYLOAD_LENGTH, value[i].length);
		}
		return messageData;
	}
	
	
	class MsgDataPerEntity{
		Map<Integer, Object> dataPerSeq;
		
		public MsgDataPerEntity() {
			dataPerSeq = new HashMap<Integer, Object>();
		}
		
		public void put(byte[] b) {
			int seq = ByteArrayManip.getIntFromByteArray(b, SEQ_OFFSET);
			int numOfParts = ByteArrayManip.getIntFromByteArray(b, NUM_OF_PARTS_OFFSET);
			int partsSeq = ByteArrayManip.getIntFromByteArray(b, PARTS_SEQ_OFFSET);
			if(partsSeq >= numOfParts) {
				throw new IllegalArgumentException();
			} 
			byte[] data = new byte[b.length - PAYLOAD_OFFSET];
			byte[][] value;
			if(!dataPerSeq.containsKey(seq)) {
				value = new byte[numOfParts][];
				dataPerSeq.put(seq, value);
			} else {
				value = (byte[][])dataPerSeq.get(seq);	
			}
			value[partsSeq] = data;
			System.arraycopy(b, PAYLOAD_OFFSET, data, 0, b.length - PAYLOAD_OFFSET);
		}
		
		public boolean isFullMessage(int seq) {
			byte[][] value = (byte[][])dataPerSeq.get(seq);
			int numOfParts = value.length;
			boolean fullMessageReceived = true;
			for(int i = 0; i < numOfParts; i ++) {
				fullMessageReceived &= value[i]!=null;
			}
			return fullMessageReceived;
		}
		
		public byte[][] getMsgData(int seq){
			return (byte[][])dataPerSeq.get(seq);
		}
	}
}
